namespace MemoryTest_Communications.PropertySearch
{
    using System;
    using System.Device.Location;
    using System.Linq;
    using System.Reactive;
    using System.Reactive.Concurrency;
    using System.Reactive.Linq;
    using System.Reactive.Subjects;
    using System.Text;
    using Model;
    using Newtonsoft.Json;
    using Resources;
    using WP7Contrib.Caching;
    using WP7Contrib.Collections;
    using WP7Contrib.Common.Helpers;
    using WP7Contrib.Communications;
    using WP7Contrib.Logging;
    using Location = Model.Location;

    public sealed class PropertySearchService : ISearchForProperties
    {
        private const string FailedNestoria = "PropertySearchServices: Failed to call Nestoria web service, message - '{0}'";

        private readonly IThrottleRequestsByTime throttler;
        private readonly IEncodeProperties propertyEncoder;
        private readonly ICacheProvider cacheProvider;
        private readonly ILog log;
        private readonly IHandleResources resourceClient;
        private readonly string action;
        private readonly string version;
        private readonly string encoding;
        private readonly TimeSpan cacheTimeout;

        public PropertySearchService(IHandleResources resourceClient,
                                     IThrottleRequestsByTime throttler,
                                     IEncodeProperties propertyEncoder,
                                     ICacheProvider cacheProvider,
                                     ILog log)
        {
            this.resourceClient = resourceClient;
            this.throttler = throttler;
            this.propertyEncoder = propertyEncoder;
            this.cacheProvider = cacheProvider;
            this.log = log;

            var definitions = JsonHandlerDefinitions.Default;
            definitions.NullValueHandling = NullValueHandling.Include;

            var resourceType = ResourceType.Json;
            this.resourceClient.ForType(resourceType)
                .WithHandlerDefinitions(definitions)
                .UseUrlForGet("http://api.nestoria.co.uk:80/api?{0}");

            this.action = string.Format("action={0}", this.propertyEncoder.Encode("search_listings"));
            this.version = string.Format("&version={0}", this.propertyEncoder.Encode(""));
            this.encoding = string.Format("&encoding={0}", this.propertyEncoder.Encode("json"));

            this.cacheTimeout = new TimeSpan(0, 0, 0, 1);
        }

        public IObservable<PropertyResult> Search(int pageIndex, int pageSize, PropertyCriteria criteria)
        {
            try
            {
                var observable = new Subject<PropertyResult>();
                var searchCriteria = new SearchCriteria(pageIndex, pageSize, criteria.DeepClone());
                var propertyResult = this.cacheProvider.Get<SearchCriteria, PropertyResult>(searchCriteria);
                if (propertyResult != null)
                {
                    this.log.Write("PropertySearchServices: Results retrieved from cache, hash code - " + criteria.GetHashCode());
                    return Observable.Return(propertyResult).AsObservable();
                }

                // Build query string parameters - start with search infrastructure values...
                var sb = new StringBuilder();
                sb.Append(this.action);
                sb.Append(this.version);
                sb.Append(this.encoding);
                sb.AppendFormat("&page={0}", pageIndex);
                sb.AppendFormat("&number_of_results={0}", pageSize);

                // Add the search critieria values to the string builder...
                sb.Append(criteria.SerializeToString(this.propertyEncoder));
                var @params = new object[] { sb.ToString() };

                Observable.Return(new Unit())
                    .Delay(this.throttler.WaitFor)
                    .SubscribeOn(Scheduler.ThreadPool)
                    .ObserveOn(Scheduler.ThreadPool)
                    .Subscribe(x => this.resourceClient.Get<Result>(@params)
                                        .Select(ProcessResponse)
                                        .SubscribeOn(Scheduler.ThreadPool)
                                        .ObserveOn(Scheduler.ThreadPool)
                                        .Subscribe(pr =>
                                                       {
                                                           this.cacheProvider.Add(searchCriteria, pr, this.cacheTimeout);
                                                           observable.OnNext(pr);
                                                       }, observable.OnError, () =>
                                                                                  {
                                                                                      observable.OnCompleted();
                                                                                      GC.Collect();
                                                                                  }),
                               exn => this.log.Write("PropertySearchServices: Throttle failed, message - '{0}'", new object[] { exn.Message }),
                               () => this.log.Write("PropertySearchServices: Throttle completed..."));
               
                return observable;
            }
            catch (Exception exn)
            {
                var message = string.Format(FailedNestoria, exn.Message);
                this.log.Write(message);
                throw new Exception(message, exn);
            }
        }
      
        private static PropertyResult ProcessResponse(Result result)
        {
            var locations = result.Response.Locations.Select(Map).ToList();
            var properties = new PagedList<Property>{Items = result.Response.Listings.Select(Map).ToList() };
                
            var pageSizeDouble = Convert.ToDouble(result.Response.ResultsCount) / result.Response.PageCount;
            
            properties.Total = Convert.ToInt32(result.Response.ResultsCount);
            properties.PageIndex = result.Response.Page;
            properties.PageSize = (int)Math.Round(pageSizeDouble);

            return new PropertyResult
                       {
                           Properties = properties,
                           ResponseCode = result.Response.ApplicationResponseCode,
                           ResponseText = result.Response.ApplicationResponseText
                       }.AddLocations(locations);
        }

        private static Property Map(Listing listing)
        {
            var model = new Property
                               {
                                   BathroomCount = listing.BathroomCount,
                                   BedroomCount = listing.BedroomCount,
                                   DatasourceName = listing.DatasourceName,
                                   Guid = listing.Guid,
                                   GuidePriceMax = Convert.ToDecimal(listing.GuidePriceMax),
                                   GuidePriceMin = Convert.ToDecimal(listing.GuidePriceMin),
                                   ImageUrl = listing.ImageUrl,
                                   Latitude = Convert.ToDecimal(listing.Latitude),
                                   ListerName = listing.ListerName,
                                   ListerUrl = listing.ListerUrl,
                                   ListingType = ListingTypeFromString(listing.ListingType),
                                   LocationAccuracy = ConversionHelper.ToInt32(listing.LocationAccuracy),
                                   Longitude = Convert.ToDecimal(listing.Longitude),
                                   Price = Convert.ToDecimal(listing.Price),
                                   PriceCurrency = listing.PriceCurrency,
                                   PriceFormatted = listing.PriceFormatted,
                                   PriceType = PriceTypeFromString(listing.PriceType),
                                   PropertyType = PropertyTypeFromString(listing.PropertyType),
                                   RoomCount = listing.RoomCount,
                                   Summary = listing.Summary,
                                   Title = listing.Title,
                                   UpdatedInDays = listing.UpdatedInDays,
                                   UpdatedInDaysFormatted = listing.UpdatedInDaysFormatted
                               };

            return model;
        }

        private static Location Map(Resources.Location location)
        {
            var model = new Location
            {
                PlaceName = location.PlaceName,
                Title = location.Title,
                LongTitle = location.LongTitle,
                CentrePoint = new GeoCoordinate(Convert.ToDouble(location.CentreLatitude),
                                                Convert.ToDouble(location.CentreLongitude))
            };

            return model;
        }

        private static PropertyType PropertyTypeFromString(string value)
        {
            return string.IsNullOrEmpty(value)
                       ? PropertyType.Undefined
                       : ConversionHelper.ParseEnum(value, () => PropertyType.Undefined);
        }

        private static ListingType ListingTypeFromString(string value)
        {
            return string.IsNullOrEmpty(value)
                       ? ListingType.Undefined
                       : ConversionHelper.ParseEnum(value, () => ListingType.Undefined);
        }

        private static PriceType PriceTypeFromString(string value)
        {
            return string.IsNullOrEmpty(value)
                       ? PriceType.Undefined
                       : ConversionHelper.ParseEnum(value, () => PriceType.Undefined);
        }
    }
}